treat string partition ranges as sensitive data#4501
Conversation
❌ 2 Tests Failed:
View the top 2 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
❌ Test FailureAnalysis: The e2e test Test_MySQL_String_Partition_Key_Sensitive_Range_Handling fails deterministically with a pgx scan mismatch ("number of field descriptions must equal number of destinations, got 2 and 3") across all 4 matrix variants, indicating a real code bug in this PR rather than a flaky failure. |
15d2fe1 to
f6df0b1
Compare
❌ Test FailureAnalysis: A new MySQL→ClickHouse e2e test added in this PR fails deterministically with a pgx scan column-mismatch ("got 2 and 3") in both suite variants, indicating a real code bug rather than a flaky/timeout/race failure. |
9816583 to
c926f03
Compare
❌ Test FailureAnalysis: The PR's own new test Test_MySQL_String_Partition_Key_Sensitive_Range_Handling fails with a deterministic assertion error (clickhouse_mysql_test.go:2096) on the MySQL→ClickHouse path across all four MySQL/MariaDB matrix jobs, indicating a real bug in the PR's string-partition-range handling rather than flakiness. |
❌ Test FailureAnalysis: The PR's own newly-added test Test_MySQL_String_Partition_Key_Sensitive_Range_Handling fails deterministically across all four MySQL/MariaDB matrix configs with a consistent "Should NOT be empty" data assertion, indicating a real bug in the PR's sensitive-partition-range feature rather than flakiness. |
f3e2322 to
5b2f94d
Compare
Code ReviewBug: File:
Fix: Change Also checked for CLAUDE.md / REVIEW.md compliance — no issues found. |
| } | ||
| insertRows = append(insertRows, []any{parentMirrorName, runUUID, partition.PartitionId, key.ID, encrypted}) | ||
|
|
||
| partition.Range = &protos.PartitionRange{ |
There was a problem hiding this comment.
Add an Offloaded: true somewhere? Would be ideal to also fail at consumption points if it's accidentally true but haven't checked how feasible that is.
There was a problem hiding this comment.
Alternative: add offloaded_range to the oneof range proto and at least have to handle it in all the switches
| if partition == nil || partition.Range == nil { | ||
| continue | ||
| } | ||
| // String ranges may contain PII data like email, so deem it as sensitive. |
There was a problem hiding this comment.
I'm leaning towards doing this for all types, just because you can extract signal from things and we want to be able to snapshot all sorts of tables. Let's discuss with the team?
There was a problem hiding this comment.
discussed with team: conclusion is to encrypt this for all data types, as integer PKs may potentially contain sensitive data as well like government-issued IDs; especially given that we also support secondary indices as partition keys.
| -- through Temporal. Today this is used only for sensitive partition ranges. | ||
| -- The table name and columns are deliberately generic so a future change can | ||
| -- offload all partition ranges to catalog to stay under Temporal's payload limit. | ||
| CREATE TABLE IF NOT EXISTS metadata_qrep_partition_ranges ( |
There was a problem hiding this comment.
Have you thought about limiting retention? I think something like indexed inserted_at and a scheduled task that wipes everything older than 30 days should be about it. Going too low would risk breaking unfortunate long snapshots or complicating the retention logic, blanket 30 seems like a reasonable default
There was a problem hiding this comment.
Inserted_at makes a lot of sense.
b4d9ba9 to
478c3f2
Compare
| jobName string, | ||
| startTime time.Time, | ||
| ) error { | ||
| if partition.RangeOffloaded { |
There was a problem hiding this comment.
this was missed earlier but we also serialize the entire QRepPartition in metadata_qrep_partitions.
so here we sanitize this as well.
❌ Test FailureAnalysis: A deterministic regression in the PR's own QRep partition-range code: Test_Offload_Partition_Ranges fails identically across all three matrix shards with a fixed "expected int(8), got int32(1)" assertion, cascading into the other QRep/API tests. |
478c3f2 to
11f7956
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
❌ Test FailureAnalysis: The same partition-range/QRep tests (Test_Offload_Partition_Ranges, TestApiPg/My TestQRep with "should have 2 items but has 3") fail deterministically across all three matrix jobs, directly matching this PR's partition-range changes, indicating a real regression rather than a flaky failure. |
7f06742 to
88636d6
Compare
|
Code review: 1 issue found. Checked for bugs and CLAUDE.md compliance. Bug: QRepHasNewRows does not accept runUUID - offloaded partition ranges never restored. File: flow/workflows/qrep_flow.go:470. runUUID is passed as a third activity argument but the function signature only accepts (ctx, config, last). Temporal silently drops extra args. Since runUUID is never received, RestoreOffloadedPartitionRanges is never called inside QRepHasNewRows. When last.RangeOffloaded==true (Range is nil), the nil check short-circuits and returns maxValue!=nil which is always true for non-empty tables. Fix: Add runUUID string to QRepHasNewRows signature and call RestoreOffloadedPartitionRanges at start. See inline comment for details. |
❌ Test FailureAnalysis: All four failing tests are QRep sync operations that time out identically across every matrix job, matching exactly this PR's change to QRep partition-range handling ("treat string partition ranges as sensitive data") — a deterministic regression, not flakiness. |
❌ Test FailureAnalysis: A consistent set of QRep tests (including the PR's own Test_Offload_Partition_Ranges) time out identically across all matrix jobs, indicating the "treat partition ranges as sensitive data" change broke the offload/restore path so QRep syncs never complete — a real bug, not a flaky failure. |
88636d6 to
f45b4ce
Compare
|
addressed review feedback:
additional changes:
what this PR doesn't cover:
|
| // Clone rather than mutate in place because this partition is shared with the pull | ||
| // goroutine, cloning keeps us from relying on implicit ordering to avoid a write race. | ||
| // TODO: drop the sync_partition column entirely; nothing reads it back. | ||
| partition = proto.Clone(partition).(*protos.QRepPartition) |
There was a problem hiding this comment.
| partition = proto.Clone(partition).(*protos.QRepPartition) | |
| partition = proto.CloneOf(partition) |
- decouple offload and restore (so restore derive based on range_offloaded flag) - always encrypt non-nil ranges regardless of type - update schema to include inserted_at timestamp - make sure metadata_qrep_partitions also does not store serialized partition in plaintext - add dynconf PEERDB_OFFLOAD_PARTITION_RANGES to allow disabling offload/encryption behind a FF - update tests
f45b4ce to
9bab62a
Compare
🔄 Flaky Test DetectedAnalysis: Kafka e2e tests timed out at identical durations receiving empty messages (broker delivery/infra issue) and a Temporal continue-as-new count race ("1 not greater than 1") failed in only one matrix variant, both unrelated to this PR's partition-range change — classic flaky signatures. ✅ Automatically retrying the workflow |
🔄 Flaky Test DetectedAnalysis: A single-matrix e2e failure where the CI MongoDB container transiently reported oplogMinRetentionHours=23 despite being started with --oplogMinRetentionHours 24, causing a validation error unrelated to the PR's partition-range sensitive-data changes. ✅ Automatically retrying the workflow |
Before introducing parallel snapshotting for arbitrary string watermark column, in order to avoid sensitive data from being accidentally exposed in db/Temporal as plaintext, this PR will offload partition ranges to db (encrypted) before return the
GetQRepPartitionsActivity result, and then restored in theReplicateQRepPartitionsActivity, to avoid crossing Temporal boundary. Long term, offloading all partition ranges to DB is also ideal, in order to not having to worry about Temporal payload limit.This PR introduces a new table
metadata_qrep_partition_rangesfor storing partition ranges. I also evaluated building on top of existingpeerdb_stats.qrep_partitionsbut that table is intended for monitoring so adding operational logic to it is not ideal. This PR also makes sure that sensitive ranges are removed before writing topeerdb_stats.qrep_partitions, which are not being used afaik.Open to feedback on what we should deem as sensitive that should be encrypted (e.g. all string vs. non-UUIDs vs. only encrypt email-shaped string). Currently all string ranges are encrypted.
Testing: add integration test coverage for offload/restore correctness logic; as well as e2e test for the workflow, and that table is cleanup when mirror is dropped.
Fixes: DBI-869